Clojureでリバーシを作ってみる - Part3 周りの石をひっくり返せるようにしてみる
ここまででリバーシ感が少し出てきていますが、ユーザーが石をひとつ置いたらひっくり返りうる箇所すべてがひっくり返ってくれると嬉しいです。ということでこのページではそういう機能を実装してみようと思います。
https://gyazo.com/f40380261ca951798f77f2bcf16f98e5
図1: 新しく石を置きたい
まずは石を置くとどうなるかを考えます。上の図のようにE6に黒石を置きます。するとE6に置く黒石とE4に置かれている黒石でE5の白石を挟むことができたので、E5を黒石にすることができます。このリバーシのルールを改めて言語化すると以下のようになります。 石を置く場合は、相手の色の石を自分の色の石ではさむように置く
縦横斜め、8方向に対して相手の色の石をはさむことができる
自分の色の石がはさんだ相手の色の石はすべて自分の色の石にしなければいけない
相手の色の石をはさめないとき石を新たに置くことはできない
ここでは石を置けるかどうかはひとまず考えないことにして、新しい石を置いたときに間にはさまっている相手の色の石をひっくり返すことだけを考えていきます。まずは簡単化のために以下の図のような状況を考えます。B3に黒石を置く場合、E3に黒石があるためC3, D3の白石を黒石にすることができます。
https://gyazo.com/b8f8b62621ae0a8fbb874805d798f75f
図2: 石の列を取り出したい
これをどうやってプログラムに落とし込むか考えてみます。いろいろと考え方はあると思いますが、ここでは単純化して考えてみます。(1)ある方向に続いている石をリストとしてすべて取り出してみて、(2)自分の石の色と異なるものと自分の石の色の境目で区切って、(3)境目から自分が置いた石側に自分の石の色と異なるものがあって、かつ、自分の石の色ではさめている石があればそれがひっくり返す対象となります。
これをClojureで書いていきます。まずは(1)のある方向に続いている石をリストとしてすべて取り出してみます。ある方向に続いている石を取り出すということで、必要な情報は置かれている石の情報、石を置く座標、方向の3つの情報があれば書けます。ここで言う方向は石を置く場所から見たときのもので、例えば上の図の例であればx軸方向に+1、y軸方向が±0の方向だということが分かります(下の図を参照)。 https://gyazo.com/8fb6634b10a85e8b42c79a06116dd153
図3: 方向の表現方法
また任意の方向に続く石の列ということなので、再帰的に書くことができます。これをそのままコードに落とすと以下のようなline-of-stonesという関数ができます。 code:clojure
(defn line-of-stones
(loop [x (+ x x')
y (+ y y')
collected []]
(if-let [stone (get stones x y)] (recur (+ x x')
(+ y y')
(conj collected [x y stone])) collected)))
第2引数posは新しく置く石の座標、第3引数directionは方向を示しています。loopの初期値は新しく置く石の座標そのものではなく、隣接する石からになっています。また、石の座標と石の色を集めたいので[[x y] stone]というベクターを、collectedへとconjしています。これをREPLで簡単に試すと次のようになります。 code:clojure
reversi.core> (defn line-of-stones
(loop [x (+ x x')
y (+ y y')
collected []]
(if-let [stone (get stones x y)] (recur (+ x x')
(+ y y')
(conj collected [x y stone])) collected)))
reversi.core> (line-of-stones stones 1 3 1 0) stonesを適当な値に変えて試してみると、おおむね期待どおりに動いていることが分かると思います。次に(2)自分の石の色と異なるものと自分の石の色の境目で区切る、ということをやってみます。図2のような石の列は[[[2 3] :w] [[3 3] :w] [[4 3] :b] [[5 3] :b] [[6 3] :w]]というふうに表現していますが、これを二分するだけであればsplit-withという関数が使えます。
code:clojure
reversi.core> (split-with #(= :w (second %)) (line-of-stones stones 1 3 1 0)) こうすれば自分の色の石との間にはさまっている相手の色の石があるかどうかが分かります。こうすると(3)では二分した結果の後方(右側)の要素が1つ以上あるかどうかで、前方がひっくり返す対象として妥当か分かりそうです。あとはこれを縦横斜めの8方向分行えばいいだけです。ここまでの話をまとめてreversi.commandというネームスペースを作りました。
code:src/reversi/command.clj
(ns reversi.command)
(defn- line-of-stones
(loop [x (+ x x')
y (+ y y')
collected []]
(if-let [stone (get stones x y)] (recur (+ x x')
(+ y y')
(conj collected [x y stone])) collected)))
(def ^:private directions
(remove (partial every? zero?))))
(->> (line-of-stones stones pos direction)
(split-with #(-> % second reversed-color?)))] (cond-> targets
(seq opposite) (into (map first sandwiched)))))
[]
directions)))
(assoc stones pos color))
stones
(conj (reverse-targets stones pos color) pos)))
line-of-stonesは上で登場したままで、reverse-targetsでは縦横斜めの8方向に関して情報を集めています。最後にreverse-targetsをput-stoneという関数から呼び出して、置かれた石の情報を更新しています。put-stoneは置かれた石の情報を更新して新しい置かれた石の情報を返します。
code:clojure
reversi.core> (store/reset-stones!)
;;=> nil
reversi.core> (swap! store/stones command/put-stone 4 5 :b) reversi.core> (println (view/render-board @store/stones))
A | B | C | D | E | F | G | H
+-------------------------------+
1 | | | | | | | | |
---+---+---+---+---+---+---+---+---+
2 | | | | | | | | |
---+---+---+---+---+---+---+---+---+
3 | | | | | | | | |
---+---+---+---+---+---+---+---+---+
4 | | | | ○ | ● | | | |
---+---+---+---+---+---+---+---+---+
5 | | | | ● | ● | | | |
---+---+---+---+---+---+---+---+---+
6 | | | | | ● | | | |
---+---+---+---+---+---+---+---+---+
7 | | | | | | | | |
---+---+---+---+---+---+---+---+---+
8 | | | | | | | | |
+-------------------------------+
;;=> nil
何度か交互に黒と白の石をput-stone関数経由で置いてみると、リバーシらしく機能していることが確認できます。